package me.zhengjie.service;

import me.zhengjie.dto.Web3jDto;
import me.zhengjie.exception.BlockChainBaseException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.datatypes.Type;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.EthCall;
import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.utils.Numeric;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

@Slf4j
@Service
@AllArgsConstructor
public class Web3jService {

    /**
     * 发起交易
     * @param web3jDto
     * @param credentials 调用者
     * @param function 封装方法
     * @return
     */
    public String sendTransaction(Web3jDto web3jDto, Credentials credentials, Function<BigInteger, RawTransaction> function) {
        // 获取nonce
        BigInteger nonce = this.getTransactionCount(web3jDto.getWeb3j(), credentials.getAddress());

        //离线创建交易
        RawTransaction rawTransaction = function.apply(nonce);

        //离线签名交易
        String hexValue = this.offlineSignTransaction(rawTransaction, credentials, web3jDto.getChainId());
        log.info("hexRawValue={}", hexValue);

        //广播交易
        return this.broadcastTransaction(web3jDto.getWeb3j(), hexValue);
    }

    /**
     * 离线签名交易
     * @param rawTransaction
     * @param credentials
     * @param chainId
     * @return
     * @throws Exception
     */
    public String offlineSignTransaction(RawTransaction rawTransaction, Credentials credentials, BigInteger chainId) {
        // 离线签名交易(chainId用于解决私链在测试过程中报“only replay-protected (EIP-155) transactions allowed over RPC”错误的问题)
        byte[] signedMessage;
        if (chainId == null) {
            signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        } else {
            signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId.longValue(), credentials);
        }
        return Numeric.toHexString(signedMessage);
    }

    /**
     * 获取账户上发生交易的笔数
     * @param web3j
     * @param address 发起地址
     * @return
     * @throws Exception
     */
    public BigInteger getTransactionCount(Web3j web3j, String address) {
        try {
            EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount(address,
                    DefaultBlockParameterName.PENDING) // 使用 pending 类型，统计交易数量时会把处理中的计算进去
                    .sendAsync()
                    .get();
            return ethGetTransactionCount.getTransactionCount();
        } catch (Exception e) {
            String message = "获取交易nonce异常：" + e.getMessage();
            throw new BlockChainBaseException(message);
        }
    }

    /**
     * 链上广播
     * @param web3j
     * @param hexValue
     * @return
     * @throws Exception
     */
    public String broadcastTransaction(Web3j web3j, String hexValue) {
        try {
            EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
            if (ethSendTransaction.getError() != null) {
                throw new RuntimeException(ethSendTransaction.getError().getMessage());
            }
            String transactionHash = ethSendTransaction.getTransactionHash();
            log.info("txId = {}", transactionHash);
            return transactionHash;
        } catch (Exception e) {
            String message = "区块链广播异常：" + e.getMessage();
            throw new BlockChainBaseException(message);
        }
    }


    /**
     * 公共查询调用方法
     * @param web3jDto
     * @param function
     * @param owner
     * @param contractAddress
     * @return
     */
    public List<Type> callFunction(Web3jDto web3jDto, org.web3j.abi.datatypes.Function function, String owner, String contractAddress) {
        try {
            String data = FunctionEncoder.encode(function);
            Transaction transaction = Transaction.createEthCallTransaction(owner, contractAddress, data);
            EthCall ethCall = web3jDto.getWeb3j().ethCall(transaction, DefaultBlockParameterName.LATEST).sendAsync().get();
            if (ethCall.getError() != null) {
                throw new RuntimeException(ethCall.getError().getMessage());
            }
            List<Type> retList = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
            if (retList == null) {
                retList = new ArrayList<>(0);
            }
            return retList;
        } catch (Exception e) {
            String message = "callFunction error! funtion=" + function.toString();
            throw new BlockChainBaseException(message);
        }
    }
}
